package com.qen.truelicense.license;

import com.qen.truelicense.util.ObfuscatedString;
import com.qen.truelicense.xml.GenericCertificate;
import com.qen.truelicense.xml.PersistenceService;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;

/**
 * This notary knows how to sign and verify a {@link GenericCertificate}.
 * <p>
 * This class is <em>not</em> thread safe.
 *
 * @author licz
 */
public class LicenseNotary {

    /**
     * The buffer size for I/O.
     */
    private static final int BUFSIZE = 5 * 1024;

    /**
     * => "param"
     */
    static final String PARAM = new ObfuscatedString(new long[] {0xE1F209D586F45386L, 0x8EFC34F498C140FBL}).toString();

    /**
     * => "alias"
     */
    private static final String ALIAS = new ObfuscatedString(new long[] {0x28E5C63420B92E11L, 0x1326B687F23AF1D0L}).toString();

    /**
     * => "exc.noKeyPwd"
     */
    private static final String EXC_NO_KEY_PWD = new ObfuscatedString(new long[] {0xF9C42194A29F6009L, 0xEC8B0B701C7AA02AL, 0xEBE8D8C806477B06L}).toString();

    /**
     * => "exc.noKeyEntry"
     */
    private static final String EXC_NO_KEY_ENTRY = new ObfuscatedString(new long[] {0xD6F5CDEF0442C4BBL, 0x93282FD6EEC9E353L, 0x72B7E7063BE74A73L}).toString();

    /**
     * => "exc.privateKeyOrPwdIsNotAllowed"
     */
    private static final String EXC_PRIVATE_KEY_OR_PWD_IS_NOT_ALLOWED = new ObfuscatedString(new long[] {0xB07E4563009BF662L, 0xE8CFF282B8907321L, 0x196F033B7D2B5057L, 0x613F0DDE76EB3CA7L, 0x431B4D91A3E3E459L}).toString();

    /**
     * => "exc.noCertificateEntry"
     */
    private static final String EXC_NO_CERTIFICATE_ENTRY = new ObfuscatedString(new long[] {0xCDA854BD46C61DB5L, 0x1CF8CDEAC7984C0L, 0x2FE8429BA9C64746L, 0x212ADF963098F37CL}).toString();

    /**
     * => "SHA1withDSA"
     */
    private static final String SHA1_WITH_DSA = new ObfuscatedString(new long[] {0xA1077CD404A1414AL, 0xE1BCC8460C21097DL, 0x5CC373B50442CCECL}).toString();

    /**
     * => "SHA256withDSA"
     */
    private static final String SHA256withDSA = new ObfuscatedString(new long[] {0x2F36BA3BC490115BL, 0xDC977123C471B704L, 0x104D4248E5674562L}).toString();

    /**
     * => "JKS"
     */
    private static final String JKS = new ObfuscatedString(new long[] {0x40A5A661CBB0BDF8L, 0x5646B5F6649945C9L}).toString();

    private KeyStoreParam param; // init by setKeyStoreParam() - should be accessed via getKeyStoreParam() only!

    private KeyStore keyStore; // init by getKeyStore()
    private PrivateKey privateKey; // lazy initialised by getPrivateKey()
    private PublicKey publicKey;  // lazy initialised by getPublicKey()

    /**
     * Creates a new License Notary.
     * <p>
     * <b>Warning:</b> The notary created by this constructor is <em>not</em>
     * valid and cannot be used unless {@link #setKeyStoreParam(KeyStoreParam)}
     * is called!
     */
    protected LicenseNotary() {
    }

    /**
     * Creates a new License Notary.
     *
     * @param param the keyStore configuration parameters
     *              - may <em>not</em> be {@code null}.
     * @throws NullPointerException     if the given parameter object does not
     *                                  obey the contract of its interface due to a {@code null}
     *                                  pointer.
     * @throws IllegalPasswordException if any password in the parameter object
     *                                  does not comply to the current policy.
     */
    public LicenseNotary(final KeyStoreParam param) {
        setKeyStoreParam0(param);
    }

    /**
     * Returns the keyStore configuration parameters.
     */
    public KeyStoreParam getKeyStoreParam() {
        return param;
    }

    /**
     * Sets the keyStore configuration parameters.
     * Calling this method resets the notary as if it had just been created.
     *
     * @param param the keyStore configuration parameters
     *              - may <em>not</em> be {@code null}.
     * @throws NullPointerException     if the given parameter object does not
     *                                  obey the contract of its interface due to a {@code null}
     *                                  pointer.
     * @throws IllegalPasswordException if any password in the parameter object
     *                                  does not comply to the current policy.
     */
    public void setKeyStoreParam(final KeyStoreParam param) {
        setKeyStoreParam0(param);
    }

    private void setKeyStoreParam0(final KeyStoreParam param) {
        // Check parameters to implement fail-fast behaviour and enforce
        // a reasonably good security level.
        if (null == param.getAlias()) {
            throw new NullPointerException(ALIAS);
        }
        final Policy policy = Policy.getCurrent();
        final String storePwd = param.getStorePwd();
        policy.checkPwd(storePwd);
        final String keyPwd = param.getKeyPwd();
        if (null != keyPwd) {
            policy.checkPwd(keyPwd);
        }
        this.param = param;
        keyStore = null;
        privateKey = null;
        publicKey = null;
    }

    /**
     * Encodes and signs the given {@code content} and returns a locked
     * generic certificate holding the encoded content and its digital
     * signature.
     * <p>
     * Please note the following:
     * <ul>
     * <li>Because this method locks the certificate, a subsequent call to
     *     {@link #sign(GenericCertificate, Object)} or
     *     {@link #verify(GenericCertificate)} is redundant
     *     and will throw a {@code PropertyVetoException}.
     *     Use {@link GenericCertificate#isLocked()} to detect whether a
     *     generic certificate has been successfuly signed or verified before
     *     or call {@link GenericCertificate#getContent()} and expect an
     *     Exception to be thrown if it hasn't.</li>
     * <li>There is no way to unlock the returned certificate.
     *     Call the copy constructor of {@link GenericCertificate} if you
     *     need an unlocked copy of the certificate.</li>
     * </ul>
     *
     * @param content the object to sign. This must either be a JavaBean or an
     *                instance of any other class which is supported by
     *                {@link PersistenceService}
     *                - maybe {@code null}.
     * @return A locked generic certificate holding the encoded content and
     * its digital signature.
     * @throws Exception for various reasons.
     */
    public GenericCertificate sign(final Object content) throws Exception {
        final GenericCertificate cert = new GenericCertificate();
        sign(cert, content);
        return cert;
    }

    /**
     * Encodes and signs the given {@code content} in the given
     * {@code certificate} and locks it.
     * <p>
     * Please note the following:
     * <ul>
     * <li>This method will throw a {@code PropertyVetoException} if the
     *     certificate is already locked, i.e. if it has been signed or
     *     verified before.</li>
     * <li>Because this method locks the certificate, a subsequent call to
     *     {@link #sign(GenericCertificate, Object)} or
     *     {@link #verify(GenericCertificate)} is redundant
     *     and will throw a {@code PropertyVetoException}.
     *     Use {@link GenericCertificate#isLocked()} to detect whether a
     *     generic certificate has been successfuly signed or verified before
     *     or call {@link GenericCertificate#getContent()} and expect an
     *     Exception to be thrown if it hasn't.</li>
     * <li>There is no way to unlock the certificate.
     *     Call the copy constructor of {@link GenericCertificate} if you
     *     need an unlocked copy of the certificate.</li>
     * </ul>
     *
     * @param certificate the generic certificate used to hold the encoded
     *                    content and its digital signature.
     * @param content     the object to sign. This must either be a JavaBean or an
     *                    instance of any other class which is supported by
     *                    {@code {@link PersistenceService}}
     *                    - maybe {@code null}.
     * @throws Exception for various reasons.
     */
    void sign(GenericCertificate certificate, Object content) throws Exception {
        certificate.sign(content, getPrivateKey(), getSignatureEngine());
    }

    /**
     * Verifies the digital signature of the encoded content in the given
     * {@code certificate} and locks it.
     * <p>
     * Please note the following:
     * <ul>
     * <li>This method will throw a {@code PropertyVetoException} if the
     *     certificate is already locked, i.e. if it has been signed or
     *     verified before.</li>
     * <li>Because this method locks the certificate, a subsequent call to
     *     {@link #sign(GenericCertificate, Object)} or
     *     {@link #verify(GenericCertificate)} is redundant
     *     and will throw a {@code PropertyVetoException}.
     *     Use {@link GenericCertificate#isLocked()} to detect whether a
     *     generic certificate has been successfuly signed or verified before
     *     or call {@link GenericCertificate#getContent()} and expect an
     *     Exception to be thrown if it hasn't.</li>
     * <li>There is no way to unlock the certificate.
     *     Call the copy constructor of {@link GenericCertificate} if you
     *     need an unlocked copy of the certificate.</li>
     * </ul>
     *
     * @param certificate the generic certificate to verify
     *                    - may <em>not</em> be {@code null}.
     * @throws Exception a subclass of this class may be thrown for various
     *                   reasons.
     */
    public void verify(GenericCertificate certificate) throws Exception {
        certificate.verify(getPublicKey(), getSignatureEngine());
    }

    /**
     * Returns the private key from the keyStore.
     *
     * @throws LicenseNotaryException    if the parameters used to access the
     *                                   corresponding key store are insufficient or incorrect.
     *                                   Note that you should always use
     *                                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                                   localized) meaningful detail message.
     * @throws IOException               if there is an I/O or format problem with the
     *                                   keyStore data.
     * @throws CertificateException      if any of the certificates in the
     *                                   keyStore could not be loaded.
     * @throws NoSuchAlgorithmException  if the algorithm used to check
     *                                   the integrity of the keyStore cannot be found.
     * @throws UnrecoverableKeyException if the key cannot get recovered
     *                                   (e.g. the given password is wrong).
     */
    protected PrivateKey getPrivateKey() throws LicenseNotaryException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
        if (null == privateKey) {
            final KeyStoreParam param = getKeyStoreParam();
            final String keyPwd = param.getKeyPwd();
            final String alias = param.getAlias();
            if (null == keyPwd) {
                throw new LicenseNotaryException(EXC_NO_KEY_PWD, alias);
            }
            final KeyStore keystore = getKeyStore();
            try {
                privateKey = (PrivateKey) keystore.getKey(alias, keyPwd.toCharArray());
            } catch (KeyStoreException keystoreIsAlreadyLoaded) {
                throw new AssertionError(keystoreIsAlreadyLoaded);
            }
            if (null == privateKey) {
                throw new LicenseNotaryException(EXC_NO_KEY_ENTRY, alias);
            }
        }
        return privateKey;
    }

    /**
     * Returns the public key from the keyStore
     *
     * @throws LicenseNotaryException   if the parameters used to access the
     *                                  corresponding key store are insufficient or incorrect.
     *                                  Note that you should always use
     *                                  {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                                  localized) meaningful detail message.
     * @throws IOException              if there is an I/O or format problem with the
     *                                  keyStore data.
     * @throws CertificateException     if any of the certificates in the
     *                                  keyStore could not be loaded.
     * @throws NoSuchAlgorithmException if the algorithm used to check
     *                                  the integrity of the keyStore cannot be found.
     */
    protected PublicKey getPublicKey() throws LicenseNotaryException, IOException, CertificateException, NoSuchAlgorithmException {
        if (null == publicKey) {
            final String alias = getKeyStoreParam().getAlias();
            final KeyStore keystore = getKeyStore();
            try {
                if ((null != getKeyStoreParam().getKeyPwd()) != keystore.isKeyEntry(alias)) {
                    throw new LicenseNotaryException(EXC_PRIVATE_KEY_OR_PWD_IS_NOT_ALLOWED, alias);
                }
                final Certificate cert = keystore.getCertificate(alias);
                if (null == cert) {
                    throw new LicenseNotaryException(EXC_NO_CERTIFICATE_ENTRY, alias);
                }
                publicKey = cert.getPublicKey();
            } catch (KeyStoreException keystoreIsAlreadyLoaded) {
                throw new AssertionError(keystoreIsAlreadyLoaded);
            }
        }
        return publicKey;
    }

    /**
     * Returns a valid signature engine to be used for signing and verifying
     * a {@link GenericCertificate} - {@code null} is never returned.
     */
    protected Signature getSignatureEngine() {
        try {
            return Signature.getInstance(SHA256withDSA);
        } catch (NoSuchAlgorithmException ex) {
            throw new AssertionError(ex);
        }
    }

    /**
     * Returns a loaded/initialized keyStore.
     *
     * @throws IOException              if there is an I/O or format problem with the
     *                                  keyStore data.
     * @throws CertificateException     if any of the certificates in the
     *                                  keyStore could not be loaded.
     * @throws NoSuchAlgorithmException if the algorithm used to check
     *                                  the integrity of the keyStore cannot be found.
     */
    protected KeyStore getKeyStore() throws IOException, CertificateException, NoSuchAlgorithmException {
        if (null != keyStore) {
            return keyStore;
        }
        InputStream in = null;
        try {
            keyStore = KeyStore.getInstance(JKS);
            in = new BufferedInputStream(param.getStream(), BUFSIZE);
            keyStore.load(in, getKeyStoreParam().getStorePwd().toCharArray());
        } catch (KeyStoreException cannotHappen) {
            throw new AssertionError(cannotHappen);
        } finally {
            if (null != in) {
                in.close();
            }
        }
        return keyStore;
    }
}
